package com.hmdp.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.hmdp.entity.Shop;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.TimeoutUtils;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

@Slf4j
// 加入ioc
@Component
public class CaCheClient {
    private final StringRedisTemplate stringRedisTemplate;

    private static final ExecutorService CACHE_REBUILD_EXCUTOR = Executors.newFixedThreadPool(10);

    public CaCheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    //    set
    public void set(String key, Object value, Long time, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }

    // 逻辑过期
    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
        RedisData redisData = new RedisData();
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        redisData.setData(value);
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    // 使用泛型
    /*
     *
     *   泛型的使用 有点遗忘了
     *   这个位置使用泛型的话 会更加合理
     *   也就是说 我直接在返回值前使用<R,T..>进行规定 这些泛型类型是自定义的
     *   然后 这个位置 使用Class<R> type 表名传递进来的类型 这样会更加合适
     * */
    public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type
            , Function<ID, R> dbFallback, Long time, TimeUnit unit) {
        String key = keyPrefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);
        // 这个位置差点给我弄出问题 ，先查如果查到 不为空 直接返回 如果为空 进行校验 是不存在 还是
        // 缓存穿透设置的值
        if (StringUtils.isNotBlank(json)) {
            return JSONUtil.toBean(json, type);
        }
        if (json != null) {
            // 解决缓存穿透问题 此时不是空 就是"" 此时返回空
            return null;
        }
        // 函数式编程
        R db = dbFallback.apply(id);
        if (db == null) {
            stringRedisTemplate.opsForValue().set(key, "", time, unit);
            return null;
        }
        this.set(key, db, time, unit);
        return db;
    }


    // 缓存击穿
    public <R, ID> R queryWithLogicalThrough(
            String keyPrefix, ID id, Class<R> type,
            Function<ID, R> dbFallback,Long nullTime,
            TimeUnit nullTimeUnit,Long cacheTime,TimeUnit cacheUnit) {
        String key = keyPrefix + id;
        String s = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(s)) {
            return JSONUtil.toBean(s, type);
        }
        if (s != null) {
            return null;
        }
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        R byId = null;
        try {
            boolean b = tryLocal(lockKey);
            if (!b) {
                Thread.sleep(50);
                // 重试 在现在这个场景要怎么解决这个问题 呢？
//                现在是封装成工具类了 我认为此时仍旧递归的话 不是很合理
//                queryWithLogicalThrough(id);
//                暂时返回空
                return null;
            }
            byId = dbFallback.apply(id);
            if (byId == null) {
                // 过期设置 空值设置过期
//                stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
                this.set(key,"",nullTime,nullTimeUnit);
                return null;
            }

//            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(byId), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
            this.set(key,JSONUtil.toJsonStr(byId),cacheTime,cacheUnit);
            //         返回数据
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
//            删除这个互斥锁
            delLocal(lockKey);
        }
//        返回数据
        return byId;
    }

    private boolean tryLocal(String key) {
        Boolean judge = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(judge);
    }

    private void delLocal(String key) {
        stringRedisTemplate.delete(key);
    }
}
